<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>AI 对话（Markdown + 流式思考）</title>

    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- marked: Markdown 解析 -->
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <!-- DOMPurify: XSS 过滤 -->
    <script src="https://cdn.jsdelivr.net/npm/dompurify@2.4.0/dist/purify.min.js"></script>

    <style>
        body { background: #faf5ff; }
        #chatWindow::-webkit-scrollbar { width: 6px; }
        #chatWindow::-webkit-scrollbar-thumb {
            background: rgba(0,0,0,0.2); border-radius: 3px;
        }
        /* 去掉 details 默认箭头 */
        details summary { list-style: none; }
    </style>
</head>
<body class="flex items-center justify-center min-h-screen p-4">
<div class="w-full max-w-3xl bg-white rounded-2xl shadow-xl flex flex-col overflow-hidden">
    <!-- 顶部 -->
    <header class="px-6 py-4 bg-[#f5f3ff] border-b border-purple-200">
        <h1 class="text-2xl font-semibold text-[#4c1d95]">My AI Assistant</h1>
    </header>

    <!-- 聊天区 -->
    <div id="chatWindow" class="flex-1 p-6 bg-[#faf5ff] overflow-y-auto space-y-4"></div>

    <!-- 输入区 -->
    <footer class="px-6 py-4 bg-white border-t border-purple-200 flex items-center space-x-4">
        <input id="inputBox" type="text" placeholder="输入消息，按回车发送…"
               class="flex-1 bg-white border border-purple-200 rounded-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-300"/>
        <button id="sendBtn"
                class="bg-[#7c3aed] hover:bg-[#6d28d9] text-white font-medium rounded-full px-6 py-2 transition">
            发送
        </button>
    </footer>
</div>

<script>
    const API_BASE = 'http://localhost:8090/api/v1/ollama';
    const MODEL    = 'deepseek-r1:1.5b';

    const chatWindow = document.getElementById('chatWindow');
    const inputBox   = document.getElementById('inputBox');
    const sendBtn    = document.getElementById('sendBtn');

    // 创建一个新气泡
    function createBubble(who) {
        const row = document.createElement('div');
        row.className = 'flex ' + (who==='assistant'?'justify-start':'justify-end');
        const bubble = document.createElement('div');
        bubble.className = `
        w-full max-w-[600px] px-5 py-3 rounded-2xl
        ${who==='assistant'?'bg-white text-gray-900 shadow':'bg-[#7c3aed] text-white'}
      `.trim();
        row.appendChild(bubble);
        chatWindow.appendChild(row);
        chatWindow.scrollTop = chatWindow.scrollHeight;
        return bubble;
    }

    // 将 Markdown 文本转成安全的 HTML 容器
    function renderMarkdown(md) {
        const raw = marked.parse(md);
        const clean = DOMPurify.sanitize(raw);
        const div = document.createElement('div');
        div.innerHTML = clean;
        return div;
    }

    /**
     * 把完整的 fullText 渲染成带可折叠思考的 DocumentFragment
     * thinkStart: 如果已开始思考，记录的时间戳（ms）
     */
    function renderFull(fullText, thinkStart) {
        const frag = document.createDocumentFragment();
        const regex = /<think>([\s\S]*?)<\/think>/gi;
        let last = 0, match;
        while ((match = regex.exec(fullText)) !== null) {
            // 普通段落
            if (match.index > last) {
                const plain = fullText.slice(last, match.index);
                const mdNode = renderMarkdown(plain);
                mdNode.childNodes.forEach(n => frag.appendChild(n));
            }
            // 思考段落
            const thinkText = match[1];
            const details = document.createElement('details');
            details.className = 'preserve-white';
            // 计算耗时
            let elapsed = thinkStart
                ? ((Date.now() - thinkStart) / 1000).toFixed(2) + 's'
                : '0.00s';
            const summary = document.createElement('summary');
            summary.className = 'flex items-center cursor-pointer';
            summary.innerHTML = `
          <span class="inline-block w-2 h-2 bg-[#a78bfa] rounded-full"></span>
          <span class="ml-2 text-[#6b21a8] font-medium">深度思考… 耗时 ${elapsed}</span>
        `;
            details.appendChild(summary);
            // 思考内容也做 Markdown 渲染
            const contentWrapper = document.createElement('div');
            contentWrapper.className = 'mt-1 text-sm italic text-[#6b21a8]';
            const mdThink = renderMarkdown(thinkText);
            mdThink.childNodes.forEach(n => contentWrapper.appendChild(n));
            details.appendChild(contentWrapper);
            frag.appendChild(details);
            // 与正式回答之间多点间隔
            const spacer = document.createElement('div');
            spacer.className = 'h-4';
            frag.appendChild(spacer);

            last = regex.lastIndex;
        }
        // 尾部普通内容
        if (last < fullText.length) {
            const tail = fullText.slice(last);
            const mdTail = renderMarkdown(tail);
            mdTail.childNodes.forEach(n => frag.appendChild(n));
        }
        return frag;
    }

    function sendMessage() {
        const txt = inputBox.value.trim();
        if (!txt) return;
        // 1. 用户气泡
        const userBubble = createBubble('user');
        userBubble.textContent = txt;
        inputBox.value = '';

        // 2. 助手气泡 & 流式接收初始化
        const asstBubble = createBubble('assistant');
        let fullText   = '';
        let thinkStart = null;

        const es = new EventSource(
            `${API_BASE}/generate_stream?model=${encodeURIComponent(MODEL)}&message=${encodeURIComponent(txt)}`
        );
        es.onmessage = evt => {
            if (evt.data === '[DONE]') return es.close();
            try {
                const pkt   = JSON.parse(evt.data);
                const chunk = pkt.result?.output?.content || '';
                const stop  = pkt.result?.metadata?.finishReason;
                if (chunk) {
                    // 首次遇到 <think> 记时
                    if (thinkStart === null && chunk.includes('<think>')) {
                        thinkStart = Date.now();
                    }
                    fullText += chunk;
                    // 每次重新渲染整个气泡内容
                    asstBubble.innerHTML = '';
                    asstBubble.appendChild(renderFull(fullText, thinkStart));
                }
                if (stop && stop.toUpperCase()==='STOP') {
                    // 完成后自动展开所有思考
                    asstBubble.querySelectorAll('details').forEach(d=> d.open = true);
                    es.close();
                }
            } catch {}
        };
        es.onerror = () => es.close();
    }

    sendBtn.addEventListener('click', sendMessage);
    inputBox.addEventListener('keydown', e => {
        if (e.key === 'Enter') sendMessage();
    });
</script>
</body>
</html>
